package nl.rug.ai.believer.model;

import java.util.ArrayList;

import nl.rug.ai.believer.model.exception.DuplicateException;
import nl.rug.ai.believer.model.exception.InvalidAgentException;
import nl.rug.ai.believer.model.exception.InvalidWorldException;

/**
 * This class represents a complete state. It contains a list of agents, worlds and relations,
 * as well a full set of equivalence classes. Finally, it is responsible for printing its contents
 * and for performing updates and upgrades.
 * @author S1849700
 *
 */
public class State implements Cloneable {

	private ArrayList<Agent> agentList = new ArrayList<Agent>();
	private ArrayList<World> worldList = new ArrayList<World>();
	
	// This is technically duplicated over all equiv classes, but useful for updates/upgrades to reconstruct them.
	private ArrayList<Relation> relationList = new ArrayList<Relation>(); 
	private ArrayList<EquivalenceClass> equivalenceClassList = new ArrayList<EquivalenceClass>(); 
	
	/** 
	 * This method can be used to copy another State into this one. It is assumed the "other" State
	 * does not change the equiv class list anymore. Otherwise this one will change with it.
	 * @param other another state, to be copied into this one.
	 */
	void copyFromOther(State other) {
		agentList = new ArrayList<Agent>(other.agentList);
		worldList = new ArrayList<World>(other.worldList);
		relationList = new ArrayList<Relation>(other.relationList);
		equivalenceClassList = new ArrayList<EquivalenceClass>(other.equivalenceClassList);
	}
	
	/**
	 * returns all worlds from the same equivalence class as the provided world. (Knowledge) 
	 * @param agent Agent for which we want knowledge.
	 * @param world The world which will be used as origin.
	 * @return All worlds existing in the equivalence class that matches this agent/world.
	 */
	World[] getKnowledgeWorlds(Agent agent, World world) {
		for (EquivalenceClass ec : equivalenceClassList) {
			if (ec.getAgent().equals(agent) && ec.hasWorld(world)) {
				return ec.getKnowledge();
			}
		}
		return null; 
	}
	
	/**
	 * Returns an ordered belief construct as contained in the equivalence class. The ordered belief
	 * returned belongs to a specific agent on a specific world.
	 * @param agent - the agent for who to retrieve the ordered belief
	 * @param world - the world for which the ordered belief needs to be found
	 * @return  - the ordered belief
	 */
	ArrayList<ArrayList<World>> getOrderedBeliefs (Agent agent, World world) {
		for (EquivalenceClass ec : equivalenceClassList) {
			if (ec.getAgent().equals(agent) && ec.hasWorld(world)) {
				return ec.getOrderedBeliefs();
			}
		}
		return null; 
	}
	
	/**
	 * This method returns an array of worlds which are believed on a specific world for a 
	 * specific agents.
	 * @param agent - the agent for which to find the beliefs
	 * @param world - the world for which to find the beliefs
	 * @return - the resulting believed worlds in array form.
	 */
	World[] getBeliefs (Agent agent, World world) {
		for (EquivalenceClass ec : equivalenceClassList) {
			if (ec.getAgent().equals(agent) && ec.hasWorld(world)) {
				return ec.getBelief();
			}
		}
		return null; 
	}

	/**
	 * returns an agent with a certain name (if found) or null (if not). (looks in the internal list 
	 * of this object)
	 * @param agent The name of the agent we are looking for
	 * @return The result
	 */
	Agent getAgent(String agent) {
		int result = agentList.indexOf(new Agent(agent));
		return (result == -1 ? null : agentList.get(result));
	}
	
	/**
	 * returns an ArrayList containing all agents known to this state.
	 * @return the list of agents
	 */
	ArrayList<Agent> getAgents() {
		return new ArrayList<Agent>(agentList);
	}

	/**
	 * returns a world with a certain name (if found), null otherwise (looks in the internal list 
	 * of this object)
	 * @param world the desired world name
	 * @return the result
	 */
	World getWorld(String world) {
		int result = worldList.indexOf(new World(world));
		return (result == -1 ? null : worldList.get(result));
	}
	
	/**
	 * Returns an ArrayList containing all worlds known to this State.
	 * @return the list of worlds
	 */
	ArrayList<World> getWorlds() {
		return new ArrayList<World>(worldList);
	}
	
	/**
	 * Check if a world exists in this state
	 * @param world world to be checked
	 * @return result true if found, false if not.
	 */
	boolean hasWorld(World world) {
		return worldList.contains(world);
	}
	
	/**
	 * This method generates a textual list of all worlds (1 name per line) that exist in this state.
	 * @return The list of Worlds
	 */
	String listWorlds() {
		StringBuffer sb = new StringBuffer();
		sb.append("Listing all world names: \n");
		for (World world : worldList) {
			sb.append(world.getName() + "\n");
		}
		sb.append("\n");
		return sb.toString();
	}
	
	/**
	 * This method creates a list of all relations that currently exist in this state, per line there will
	 * be "agent name", "from world name" and "to world name".
	 * @return The list of Relations
	 */
	String listRelations() {
		StringBuffer sb = new StringBuffer();
		sb.append("Listing all relations as <agent>, <from world>, <to world>: \n");
		for (Relation relation : relationList) {
			sb.append(relation.getAgent().getName() + ", " + relation.getFromWorld().getName() + ", " + 
					relation.getToWorld().getName() + "\n");
		}
		return sb.toString();
	}
	
	/**
	 * This method makes a list of all agents that currently exist in this state (1 name per line).
	 * @return The list of Agents
	 */
	String listAgents() {
		StringBuffer sb = new StringBuffer();
		sb.append("Listing all agent names: \n");
		for (Agent agent : agentList) {
			sb.append(agent.getName() + "\n");
		}
		sb.append("\n");
		return sb.toString();
	}
	
	/**
	 * This method makes a list of all equivalence classes which currently exist in this state.
	 * format is line 1: number, line 2: agent name, line 3: worlds, and then repeat for each 
	 * equivalence class.
	 * @return - the list of equivalence classes
	 */ 
	String listEquivalenceClasses() {
		StringBuffer sb = new StringBuffer();
		sb.append("Listing all equivalence classes");
		for (int i = 0; i < equivalenceClassList.size(); i++) {
			sb.append("\n\nEquivalence Class nr. " + i + "\n");
			sb.append("Agent Name: " + equivalenceClassList.get(i).getAgent().getName() + "\n");
			sb.append("Worlds: ");
			for (World w : equivalenceClassList.get(i).getKnowledge()) {
				sb.append(w.getName() + ", ");
			}
			sb.deleteCharAt(sb.length() - 2); // delete the comma at the end of the last item
		}
		return sb.toString();
	}
	
	/**
	 * Method to add an agent to this state
	 * @param agent - the agent to be added
	 */
	void addAgent(Agent agent) {
		if (agentList.indexOf(agent) != -1) {
			throw new DuplicateException("Duplicate agent: " + agent.getName());
		}
		agentList.add(agent);
	}
	
	/**
	 * Method to add a world to this state
	 * @param world - the world to be added
	 */
	void addWorld(World world) {
		if (worldList.indexOf(world) != -1) {
			throw new DuplicateException("Duplicate world: " + world.getName());
		}
		worldList.add(world);
	}
	
	/**
	 * Method to add a relation to this state
	 * @param relation - the relation to be added
	 */
	void addRelation(Relation relation) {
		if (relationList.contains(relation)) {
			throw new DuplicateException("Duplicate relation, agent: " + relation.getAgent().getName()
					+ " from: " + relation.getFromWorld().getName() + " to: " + 
					relation.getToWorld().getName());
		}
		if (!agentList.contains(relation.getAgent())) {
			throw new InvalidAgentException("Cannot add a relation for which no agent exists (" 
					+ relation.getAgent().getName() + ")");
		}
		if (!worldList.contains(relation.getFromWorld())) {
			throw new InvalidWorldException("Cannot add a relation for which the 'from world' does not exist (" 
					+ relation.getFromWorld().getName() + ")");
		}
		if (!worldList.contains(relation.getToWorld())) {
			throw new InvalidWorldException("Cannot add a relation for which the 'to world' does not exist (" 
					+ relation.getToWorld().getName() + ")");
		}
		relationList.add(relation); 
		
		addToEquivalenceClass(relation);
	}
	
	/**
	 * Support method to add a relation to the proper equivalence class. Also responsible for merging 
	 * equivalence classes if need be.
	 * @param relation - the relation to be added
	 */
	private void addToEquivalenceClass(Relation relation) {
		EquivalenceClass existsIn1 = null;
		EquivalenceClass existsIn2 = null;
		
		for (EquivalenceClass ec : equivalenceClassList) {
			if (!ec.getAgent().equals(relation.getAgent())) {
				continue; // not the same agent means we're not interested.
			}
			// EquivalenceClass will catch any duplicates that arise from this.
			if (ec.hasWorld(relation.getFromWorld()) || ec.hasWorld(relation.getToWorld())) {
				if (existsIn1 == null) {
					existsIn1 = ec;
				}
				else {
					if (existsIn2 != null) {
						throw new RuntimeException("Found 3 matching Equivalence Classes to 1 relation, 2 is max.");
					}
					existsIn2 = ec;
				}
			}
		}
		// Adding a relation to an equiv class.
		if (existsIn1 != null && existsIn2 == null) {
			existsIn1.addRelation(relation);
		}
		// merging 2 equiv classes if needed
		else if (existsIn1 != null && existsIn2 != null) {
			Util.debug("Found worlds in this relation referenced in 2 equivalence classes, merging them.");
			Util.debug("Relation belongs to agent " + relation.getAgent().getName() + " from world " +
					relation.getFromWorld().getName() + " to world " + relation.getToWorld().getName());
			
			existsIn1.addRelation(relation);
			for (Relation rel : existsIn2.getRelations()) {
				existsIn1.addRelation(rel);
			}
			equivalenceClassList.remove(existsIn2);
		}
		// Creating a new equiv class for this relation (these worlds)
		else {
			EquivalenceClass ec = new EquivalenceClass(relation.getAgent());
			ec.addRelation(relation);
			equivalenceClassList.add(ec);
		}
	}
	
	/**
	 * This method makes an update based on global announcement policies (no agents/private knowledge involved).
	 * Also note that this *changes* the state by deleting relations and the like, so usage of a clone is recommended.
	 * @param validWorlds the list of worlds that is "valid" and needs to be separated from the rest.
	 */
	void performUpdate(ArrayList<World> validWorlds) {
		// every delete makes this list shorter so work from back to front (or you'll need to mess with the counter)
		for (int i = relationList.size() - 1; i != 0; i--) {
			Relation target = relationList.get(i);
			// if a relation contains a valid toworld but not a valid from world (or vice versa), it needs to be deleted.
			if ( ( validWorlds.contains(target.getFromWorld()) && !validWorlds.contains(target.getToWorld()) ) ||
					( !validWorlds.contains(target.getFromWorld()) && validWorlds.contains(target.getToWorld()) ) ) {
				relationList.remove(i);
			}
		}
		// clear all equiv classes
		equivalenceClassList = new ArrayList<EquivalenceClass>();
		// and refill them.
		for (Relation rel : relationList) {
			addToEquivalenceClass(rel);
		}
	}
	
	/**
	 * This method performs a secret update for the agents listed in "agents", in which all "validWorlds"
	 * are assumed to be true. Furthermore, it will return an updated reference to the "currentWorld"
	 * which will point at the newly created world. (Rather than the old one.)
	 * @param validWorlds - all worlds on which the condition of the update is true
	 * @param agents - all agents who are aware of this update
	 * @param currentWorld - the current world
	 * @return -- an updated reference to the current world
	 */
	World performUpdate(ArrayList<World> validWorlds, ArrayList<Agent> agents, World currentWorld) {
		ArrayList<World> newWorldList = new ArrayList<World>();
		ArrayList<Relation> relList = new ArrayList<Relation>();
		World newCurrentWorld = currentWorld;
		for (World validWorld : validWorlds) {
			for (long i = 0; ; i++) {
				// if we exceed this value, we can't continue. Should not happen in normal operation
				// without extreme computing power, or it will take literally years to calculate.
				if (i >= Long.MAX_VALUE - 1) {
					throw new RuntimeException("Error: too much duplicates of World \"" + 
						validWorld.getName() + "\". Under normal conditions, this error can only be "
						+ "reached by formula's which will take close to forever (years) to validate");
				}
				World myWorld = new World(validWorld);
				// long + string = string in the form of 1234string
				myWorld.setName(i + myWorld.getName());
				// get the next number if this one already exists.
				if (worldList.contains(myWorld)) {
					continue;
				}
				worldList.add(myWorld);
				newWorldList.add(myWorld);
				if (validWorld.equals(currentWorld)) {
					newCurrentWorld = myWorld;
				}
				for (Agent myAgent : agentList) {
					// add reflexive relations for all agents.
					relList.add(new Relation(myAgent, myWorld, myWorld));
					// if agent is not in the list, he believes the old world is true.
					if (!agents.contains(myAgent)) {
						relList.add(new Relation(myAgent, myWorld, validWorld));
					}
				}
				break; // if we are here, we are done for with this world. (no more numbers needed.)
			}
		}
		for (Relation target : relationList) { 
			// if both parts of the relation are within the valid worlds, and the relation is not reflexive
			if ( validWorlds.contains(target.getFromWorld()) && validWorlds.contains(target.getToWorld())
					&& !target.getFromWorld().equals(target.getToWorld())) {
				// find which new worlds this relation was about
				World from = newWorldList.get(validWorlds.indexOf(target.getFromWorld()));
				World to = newWorldList.get(validWorlds.indexOf(target.getToWorld()));
				// add it to a temp list (no need to mess with the iterator of this loop).
				relList.add(new Relation(target.getAgent(), from, to));
			}
			// if a relation contains a valid fromworld but not a valid toworld, and agent is not listed
			if (validWorlds.contains(target.getFromWorld()) && !validWorlds.contains(target.getToWorld())
					&& !agents.contains(target.getAgent())) {
				// find which new world this relation was from
				World from = newWorldList.get(validWorlds.indexOf(target.getFromWorld()));
				// add relation from new world to old for this agent
				Relation rel = new Relation(target.getAgent(), from, target.getToWorld());
				// may be duplicate, see next if block.
				if (!relList.contains(rel)) {
					relList.add(rel);
				}
			}
			// if agent not listed *and* relation goes from outside to inside, there must be an arrow from inside to out. (connected)
			if (validWorlds.contains(target.getToWorld()) && !validWorlds.contains(target.getFromWorld())
					&& !agents.contains(target.getAgent())) {
					// get new version of ToWorld, make it the new from. These worlds may be duplicates.
					World from = newWorldList.get(validWorlds.indexOf(target.getToWorld()));
					Relation rel = new Relation(target.getAgent(), from, target.getFromWorld());
					if (!relList.contains(rel)) {
						relList.add(rel);
					}
			}
			// in all other cases (reflexive and from old to new and from new to old for agents that
			// were in the list, do nothing. (Reflexive is already added earlier for all agents.)
		}
		relationList.addAll(relList);
		
		// clear all equiv classes
		equivalenceClassList = new ArrayList<EquivalenceClass>();
		// and refill them.
		
		for (Relation rel : relationList) {
			addToEquivalenceClass(rel);
		}
		return newCurrentWorld;
	}
	
	/**
	 * This method performs a secret radical upgrade for the agents listed in "agents", in which all 
	 * "validWorlds" are assumed to be true. Furthermore, it will return an updated reference to the 
	 * "currentWorld" which will point at the newly created world. (Rather than the old one.)
	 * @param validWorlds - the worlds in which this upgrade was valid
	 * @param agents - the agents aware of the upgrade
	 * @param currentWorld - the current world
	 * @return - an updated reference to the current world
	 */
	World performRadicalUpgrade(ArrayList<World> validWorlds, ArrayList<Agent> agents, World currentWorld) {
		ArrayList<World> newWorldList = new ArrayList<World>();
		ArrayList<Relation> relList = new ArrayList<Relation>();
		ArrayList<World> targetList = new ArrayList<World>(worldList);
		World newCurrentWorld = currentWorld;
		// we want copies of all worlds for a private upgrade.
		for (World validWorld : targetList) {
			for (long i = 0; ; i++) {
				// if we exceed this value, we can't continue. Should not happen in normal operation
				// without extreme computing power, or it will take literally years to calculate.
				if (i >= Long.MAX_VALUE - 1) {
					throw new RuntimeException("Error: too much duplicates of World \"" + 
						validWorld.getName() + "\". Under normal conditions, this error can only be "
						+ "reached by formula's which will take close to forever (years) to validate");
				}
				World myWorld = new World(validWorld);
				// long + string = string in the form of 1234string
				myWorld.setName(i + myWorld.getName());
				// get the next number if this one already exists.
				if (worldList.contains(myWorld)) {
					continue;
				}
				worldList.add(myWorld);
				newWorldList.add(myWorld);
				// if the current world is equal to the world we just duplicated, set the duplicate as current.
				if (validWorld.equals(currentWorld)) {
					newCurrentWorld = myWorld;
				}
				for (Agent myAgent : agentList) {
					// add reflexive relations for all agents.
					relList.add(new Relation(myAgent, myWorld, myWorld));
					// if agent is not in the list, he believes the old world is true.
					if (!agents.contains(myAgent)) {
						relList.add(new Relation(myAgent, myWorld, validWorld));
					}
				}
				break; // if we are here, we are done for with this world. (no more numbers needed.)
			}
		}

		for (Relation target : relationList) { 
			if (target.getFromWorld().equals(target.getToWorld())) {
				continue; // skip reflexive relations, they were added before.
			}
			/* 
			 * for all agents:
			 * valid - valid / invalid - invalid should be added.
			 * valid -> invalid  should be reversed and then added.
			 * invalid -> valid should be added.
			 */
			World to;
			World from;
			// reverse these.
			if (validWorlds.contains(target.getFromWorld()) && !validWorlds.contains(target.getToWorld())) {
				to = newWorldList.get(targetList.indexOf(target.getFromWorld()));
				from = newWorldList.get(targetList.indexOf(target.getToWorld()));
			}
			else {
				from = newWorldList.get(targetList.indexOf(target.getFromWorld()));
				to = newWorldList.get(targetList.indexOf(target.getToWorld()));
			}
			relList.add(new Relation(target.getAgent(), from, to));
			
			
			/* 
			 * for unlisted agents: 
			 * arrows back from all new to all old.
			 */
			if (!agents.contains(target.getAgent())) {
				from = newWorldList.get(targetList.indexOf(target.getFromWorld()));
				to = target.getToWorld();
				relList.add(new Relation(target.getAgent(), from, to));
			}
		}
		relationList.addAll(relList);
		
		// clear all equiv classes
		equivalenceClassList = new ArrayList<EquivalenceClass>();
		// and refill them.
		
		for (Relation rel : relationList) {
			addToEquivalenceClass(rel);
		}
		return newCurrentWorld;
	}
	
	/**
	 * This method makes an Upgrade according to DEL logic. 
	 * This method *changes* this state, so it is recommended to use 
	 * only on a clone.
	 * @param validWorlds  The list of "valid" worlds to which all arrows need to go.
	 */
	void performUpgrade(ArrayList<World> validWorlds) {
		ArrayList<Relation> relList = new ArrayList<Relation>();
		for (Relation rel : relationList) {
			if (validWorlds.contains(rel.getFromWorld()) && !validWorlds.contains(rel.getToWorld())) {
				Relation temp = new Relation(rel.getAgent(), rel.getToWorld(), rel.getFromWorld());
				// if relationList has this one, it means relList will also have/get it, don't need duplicates.
				if (relationList.contains(temp)) {
					continue;
				}
				relList.add(temp);
			}
			else {
				relList.add(rel);
			}
		}

		relationList = relList;
		// clear all equiv classes
		equivalenceClassList = new ArrayList<EquivalenceClass>();
		// and refill them.
		for (Relation rel : relationList) {
			addToEquivalenceClass(rel);
		}
	}
	
	/**
	 * This method makes an Upgrade according to DEL logic. But only for 1 agent.
	 * This method *changes* this state, so it is recommended to use 
	 * only on a clone.
	 * @param validWorlds  The list of "valid" worlds to which all arrows need to go.
	 */
	void performConvervativeUpgrade(ArrayList<World> validWorlds, Agent agent) {
		ArrayList<Relation> relList = new ArrayList<Relation>();
		for (Relation rel : relationList) {
			if (!rel.getAgent().equals(agent)) {
				continue;
			}
			if (validWorlds.contains(rel.getFromWorld()) && !validWorlds.contains(rel.getToWorld())) {
				Relation temp = new Relation(rel.getAgent(), rel.getFromWorld(), rel.getToWorld());
				// if relationList has this one, it means relList will also have/get it, don't need duplicates.
				if (relationList.contains(temp)) {
					continue;
				}
				relList.add(temp);
			}
			else {
				relList.add(rel);
			}
		}
		relationList = relList;
		// clear all equiv classes
		equivalenceClassList = new ArrayList<EquivalenceClass>();
		// and refill them.
		for (Relation rel : relationList) {
			addToEquivalenceClass(rel);
		}
	}
	
	/**
	 * Method used to create a clone of this state. Mainly intended to be used before performing an 
	 * update or upgrade, so the original can be restored.
	 */
	public State clone() {
		try {
			State clone = (State) super.clone();
			clone.agentList = new ArrayList<Agent>(agentList);
			clone.worldList = new ArrayList<World>(worldList);
			clone.relationList = new ArrayList<Relation>(relationList);
			clone.equivalenceClassList = new ArrayList<EquivalenceClass>(equivalenceClassList);
			return clone;
		} 
		catch (CloneNotSupportedException cnse) {
			// can't happen, but tell Java that.
			Util.debug("A cloning exception has occured in State.clone.");
			return null;
		}
	}
}
